package rps.game;

import java.rmi.RemoteException;
import java.util.ArrayList;

import rps.client.GameListener;
import rps.game.data.AttackResult;
import rps.game.data.Figure;
import rps.game.data.FigureKind;
import rps.game.data.Move;
import rps.game.data.Player;

/**
 * The {@code GameImpl} is an implementation for the {@code Game} interface. It
 * contains the necessary logic to play a game.
 */
public class GameImpl implements Game {
	
	/** The boards rows. */
	public static final int ROWS = 6;
	/** The boards columns. */
	public static final int COLS = 7;
 
	/** GameController for player 1. */
	private GameListener listener1;
	/** GameController for player 2. */
	private GameListener listener2;
	/** The hosting player. */
	private Player player1;
	/** The joining player. */
	private Player player2;
	
	/** The moving player. */
	private Player nextMovingPlayer;
	/** The position of a drawn fight. */
	private int drawToIndex = -1;
	/** The starting position of a drawn fight. */
	private int drawFromIndex = -1;
	/** The decision of player 1 after a draw. */
	private FigureKind drawChoice1;
	/** The decision of player 2 after a draw. */
	private FigureKind drawChoice2;
	/** Indicates if the game is still running. */
	private boolean gameIsRunning = true;
	
	/** Field for storing all figures. */
	private Figure[] field = new Figure[ROWS * COLS];
	/** Stores the last move. */
	private Move lastMove;
	/** Initial assignment choice for player 1. */
	private FigureKind[] initialAssignment1;
	/** Initial assignment choice for player 2. */
	private FigureKind[] initialAssignment2;
	/** Initial choice for player 1. */
	private FigureKind initialChoice1;
	/** Initial choice for player 2. */
	private FigureKind initialChoice2;

	public GameImpl(GameListener listener1, GameListener listener2) throws RemoteException {
		this.listener1 = listener1;
		this.player1 = listener1.getPlayer();
		this.listener2 = listener2;
		this.player2 = listener2.getPlayer();
	}

	@Override
	public void sendMessage(Player p, String message) throws RemoteException {
		listener1.chatMessage(p, message);
		listener2.chatMessage(p, message);
	}

	@Override
	public void setInitialAssignment(Player p, FigureKind[] assignment) throws RemoteException {
		if(p.equals(player1)){
			if(initialAssignment1 != null){
				throw new IllegalStateException("Must not set initial assignment twice.");
			}
			else{
				initialAssignment1 = assignment;
			}
		}
		else if(p.equals(player2)){
			if(initialAssignment2 != null){
				throw new IllegalStateException("Must not set initial assignment twice.");
			}
			else{
				initialAssignment2 = assignment;
			}
		}
		
		if(initialAssignment1 != null && initialAssignment2 != null){
			for(int i=0; i<14; i++){
				field[i] = new Figure(initialAssignment2[i], player2);
			}
			for(int i=28; i<42; i++){
				field[i] = new Figure(initialAssignment1[i], player1);
			}
			
			listener1.provideInitialChoice();
			listener2.provideInitialChoice();
		}
	}

	@Override
	public void setInitialChoice(Player p, FigureKind kind) throws RemoteException {
		if(p.equals(player1)){
			if(initialChoice1 != null){
				throw new RemoteException("Must not update initial choice without beeing asked.");
			}
			else{
				initialChoice1 = kind;
			}
		}
		else if(p.equals(player2)){
			if(initialChoice2 != null){
				throw new RemoteException("Must not update initial choice without beeing asked.");
			}
			else{
				initialChoice2 = kind;
			}
		}
		
		if(initialChoice1 != null && initialChoice2 != null){
			if(initialChoice1.attack(initialChoice2).equals(AttackResult.DRAW)){
				listener1.provideInitialChoice();
				listener2.provideInitialChoice();
			}
			else{
				listener1.startGame();
				listener2.startGame();
				if(initialChoice1.attack(initialChoice2).equals(AttackResult.WIN)){
					nextMovingPlayer = player1;
					listener1.provideNextMove();
				}
				else{
					nextMovingPlayer = player2;
					listener2.provideNextMove();
				}
			}
			initialChoice1 = null;
			initialChoice2 = null;
		}
	}

	@Override
	public void move(Player movingPlayer, int fromIndex, int toIndex) throws RemoteException {
		if(!movingPlayer.equals(nextMovingPlayer)){
			throw new IllegalStateException("Player must not move multiple times.");
		}
		
		lastMove = new Move(fromIndex, toIndex, getField());
		
		if(field[toIndex] == null){
			// Just a movement
			field[toIndex] = field[fromIndex];
			field[fromIndex] = null;

			listener1.figureMoved();
			listener2.figureMoved();
			callNextPlayer();
		}
		else if(field[toIndex].belongsTo(getOpponent(movingPlayer))){
			// A fight!
			AttackResult res = field[fromIndex].attack(field[toIndex]);
			
			field[fromIndex].setDiscovered();
			field[toIndex].setDiscovered();
			
			if(res.equals(AttackResult.DRAW)){
				// DRAW
				drawToIndex = toIndex;
				drawFromIndex = fromIndex;
				
				listener1.provideChoiceAfterFightIsDrawn();
				listener2.provideChoiceAfterFightIsDrawn();
			}
			else if(res.equals(AttackResult.WIN)){
				// WIN
				field[toIndex] = field[fromIndex];
				field[fromIndex] = null;
				
				listener1.figureAttacked();
				listener2.figureAttacked();
				callNextPlayer();
			}
			else if(res.equals(AttackResult.LOOSE)){
				// LOOSE
				field[fromIndex] = null;
				
				listener1.figureAttacked();
				listener2.figureAttacked();
				callNextPlayer();
			}
			else if(res.equals(AttackResult.LOOSE_AGAINST_TRAP)){
				field[fromIndex] = null;
				field[toIndex] = null;
				
				listener1.figureAttacked();
				listener2.figureAttacked();
				callNextPlayer();
			}
			else if(res.equals(AttackResult.WIN_AGAINST_FLAG)){
				// WIN_AGAINST_FLAG
				
				endGame();
				
				listener1.figureAttacked();
				listener2.figureAttacked();
				
				if(movingPlayer.equals(player1)){
					listener1.gameIsWon();
					listener2.gameIsLost();
				}
				else{
					listener1.gameIsLost();
					listener2.gameIsWon();
				}
			}
		}
		else{
			throw new IllegalArgumentException("Must not attack own figure.");
		}
		
	}

	/**
	 * Uncovers the field when the game is won.
	 */
	private void endGame() {
		for(int i=0; i<field.length; i++)
			if(field[i] != null)
				field[i].setDiscovered();
		
		gameIsRunning = false;
	}

	@Override
	public void setUpdatedKindAfterDraw(Player p, FigureKind kind) throws RemoteException {
		if(p.equals(player1)){
			if(drawChoice1 != null){
				throw new RemoteException("Must not update draw choice without beeing asked.");
			}
			else{
				drawChoice1 = kind;
			}
		}
		else if(p.equals(player2)){
			if(drawChoice2 != null){
				throw new RemoteException("Must not update draw choice without beeing asked.");
			}
			else{
				drawChoice2 = kind;
			}
		}
		
		if(drawChoice1 != null && drawChoice2 != null){
			AttackResult res = drawChoice1.attack(drawChoice2);
			
			if(field[drawFromIndex].belongsTo(player1)){
				field[drawFromIndex] = newDiscoveredFigure(drawChoice1, player1);
				if(lastMove.getOldField()[drawFromIndex].isDiscovered()){
					lastMove.getOldField()[drawFromIndex] = newDiscoveredFigure(drawChoice1, player1);
				}
				else{
					lastMove.getOldField()[drawFromIndex] = new Figure(drawChoice1, player1);
				}

				field[drawToIndex] = newDiscoveredFigure(drawChoice2, player2);
				if(lastMove.getOldField()[drawToIndex].isDiscovered()){
					lastMove.getOldField()[drawToIndex] = newDiscoveredFigure(drawChoice2, player2);
				}
				else{
					lastMove.getOldField()[drawToIndex] = new Figure(drawChoice2, player2);
				}
			}
			else{
				field[drawToIndex] = newDiscoveredFigure(drawChoice1, player1);
				if(lastMove.getOldField()[drawToIndex].isDiscovered()){
					lastMove.getOldField()[drawToIndex] = newDiscoveredFigure(drawChoice1, player1);
				}
				else{
					lastMove.getOldField()[drawToIndex] = new Figure(drawChoice1, player1);
				}

				
				field[drawFromIndex] = newDiscoveredFigure(drawChoice2, player2);
				if(lastMove.getOldField()[drawFromIndex].isDiscovered()){
					lastMove.getOldField()[drawFromIndex] = newDiscoveredFigure(drawChoice2, player2);
				}
				else{
					lastMove.getOldField()[drawFromIndex] = new Figure(drawChoice2, player2);
				}
			}
			
			if(res.equals(AttackResult.DRAW)){
				drawChoice1 = null;
				drawChoice2 = null;
				listener1.provideChoiceAfterFightIsDrawn();
				listener2.provideChoiceAfterFightIsDrawn();
			}
			else{
				if(res.equals(AttackResult.WIN)){
					field[drawToIndex] = newDiscoveredFigure(drawChoice1, player1);
				}
				else{
					field[drawToIndex] = newDiscoveredFigure(drawChoice2, player2);
				}
				
				field[drawFromIndex] = null;
				
				drawChoice1 = null;
				drawChoice2 = null;
				drawToIndex = -1;
				drawFromIndex = -1;

				listener1.figureAttacked();
				listener2.figureAttacked();
				callNextPlayer();
			}
		}
	}

	@Override
	public void surrender(Player p) throws RemoteException {
		if(!gameIsRunning){
			throw new IllegalStateException("Cannot surrender game that is already finished.");
		}
		listener1.chatMessage(p, "Ich gebe auf...");
		listener2.chatMessage(p, "Ich gebe auf...");
		
		endGame();
		
		if(p.equals(player1)){
			listener1.gameIsLost();
			listener2.gameIsWon();
		}
		else{
			listener1.gameIsWon();
			listener2.gameIsLost();
		}
	}

	@Override
	public Figure[] getField() throws RemoteException {
		return field.clone();
	}

	@Override
	public Move getLastMove() throws RemoteException {
		return lastMove;
	}

	@Override
	public Player getOpponent(Player p) throws RemoteException {
		if(p.equals(player1)){
			return player2;
		}
		else if(p.equals(player2)){
			return player1;
		}
		else{
			throw new RemoteException("Invalid player without opponent.");
		}
	}
	
	/**
	 * Toggles moving player and calls the next player to move.
	 * @throws RemoteException
	 */
	private void callNextPlayer() throws RemoteException{
		nextMovingPlayer = getOpponent(nextMovingPlayer);
		
		if(nextMovingPlayer.equals(player1)){
			if(canPlayerMove(player1)){
				listener1.provideNextMove();
			}
			else if(canPlayerMove(player2)){
				callNextPlayer();
			}
			else{
				endGame();
				
				listener1.gameIsDrawn();
				listener2.gameIsDrawn();
			}
		}
		else{
			if(canPlayerMove(player2)){
				listener2.provideNextMove();
			}
			else if(canPlayerMove(player1)){
				callNextPlayer();
			}
			else{
				endGame();
				
				listener1.gameIsDrawn();
				listener2.gameIsDrawn();
			}
		}
	}
	
	/**
	 * Checks if a player can move one of his figures.
	 * @param p The Player trying to move.
	 * @return {@code true} if the player can move one of his figures.
	 */
	private boolean canPlayerMove(Player p){
		ArrayList<Integer> figures = getFiguresForPlayer(p);
		
		for(int i=0; i<figures.size(); i++){
			if(isMovable(figures.get(i), p))
				return true;
		}
			
		return false;
	}
	
	/**
	 * Returns all of {@code p}'s figures.
	 * @param p The Player.
	 * @return His figures.
	 */
	private ArrayList<Integer> getFiguresForPlayer(Player p){
		ArrayList<Integer> figures = new ArrayList<Integer>();
		
		for(int i=0; i<field.length; i++){
			if(field[i] != null && field[i].belongsTo(p)){
				figures.add(i);
			}
		}
		
		return figures;
	}
	
	/**
	 * Checks if a position is a movable figure for player {@code p}.
	 * @param index The index to check.
	 * @param p The player trying to move.
	 * @return True if movable.
	 */
	private boolean isMovable(int index, Player p){
		if(field[index].getKind().equals(FigureKind.FLAG) ||
			field[index].getKind().equals(FigureKind.TRAP)){
			return false;
		}
	
		// Move up
		if(getRow(index) > 0)
			if(field[index - 7] == null || !field[index - 7].belongsTo(p))
				return true;

		// Move right
		if(getCol(index) < 6)
			if(field[index + 1] == null || !field[index + 1].belongsTo(p))
				return true;
		
		// Move down
		if(getRow(index) < 5)
			if(field[index + 7] == null || !field[index + 7].belongsTo(p))
				return true;
		
		// Move left
		if(getCol(index) > 0)
			if(field[index - 1] == null || !field[index - 1].belongsTo(p))
				return true;
		
		return false;
	}
	
	/**
	 * Gets a row corresponding to an index.
	 * @param index The index.
	 * @return The corresponding row from 0 to 5.
	 */
	private int getRow(int index){
		return index/7;
	}
	
	/**
	 * Gets a column corresponding to an index.
	 * @param index The index.
	 * @return The corresponding column from 0 to 6.
	 */
	private int getCol(int index){
		return index%7;
	}
	
	/**
	 * Prints the field for testing purpose.
	 */
	@SuppressWarnings("unused")
	private void printField(){
		System.out.println();
		
		for(int i=0; i<field.length; i++){
			if(i%7==0)
				System.out.println();
			
			System.out.print(field[i]);
		}
	}
	
	private Figure newDiscoveredFigure(FigureKind fk, Player player){
		Figure f = new Figure(fk, player);
		f.setDiscovered();
		return f;
	}
}